import {useState} from 'react';
import {useSolanaClientProvider} from './useSolanaClientProvider';
import {
  ComputeBudgetProgram,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
} from '@solana/web3.js';
import {useActorIdentity} from '../useActorIdentity';
import {
  getAssociatedTokenAddress,
  createTransferCheckedInstruction,
  createAssociatedTokenAccountInstruction,
} from '@solana/spl-token';

export interface PrioritizationFees {
  maxFee: number;
  minFee: number;
  averageFeeIncludingZeros: number;
  averageFeeExcludingZeros: number;
  medianFee: number;
}

export function useSolanaTransaction() {
  const [loading, setLoading] = useState<boolean>(false);
  const {solanaClient} = useSolanaClientProvider();
  const {solanaSignTx} = useActorIdentity();

  const getRecentPrioritizationFees =
    async (): Promise<PrioritizationFees | null> => {
      try {
        const recentPrioritizationFees =
          await solanaClient!.getRecentPrioritizationFees();
        recentPrioritizationFees.sort((a, b) => {
          return b.prioritizationFee - a.prioritizationFee;
        });

        const maxFee = recentPrioritizationFees[0].prioritizationFee;
        const minFee =
          recentPrioritizationFees[recentPrioritizationFees.length - 1]
            .prioritizationFee;

        const prioritizationFees = recentPrioritizationFees.map(
          item => item.prioritizationFee,
        );
        const totalFees = prioritizationFees.reduce(
          (value, element) => value + element,
          0,
        );

        const averageFeeIncludingZeros = Math.floor(
          totalFees / recentPrioritizationFees.length,
        );
        const prioritizationFeeExcludeZero = recentPrioritizationFees.filter(
          item => item.prioritizationFee > 0,
        );

        prioritizationFeeExcludeZero.sort(
          (a, b) => a.prioritizationFee - b.prioritizationFee,
        );

        let medianFee = 0;
        if (prioritizationFeeExcludeZero.length > 0) {
          const midIndex = Math.floor(prioritizationFeeExcludeZero.length / 2);
          if (prioritizationFeeExcludeZero.length % 2 !== 0) {
            medianFee =
              prioritizationFeeExcludeZero[midIndex].prioritizationFee;
          } else {
            medianFee = Math.floor(
              (prioritizationFeeExcludeZero[midIndex - 1].prioritizationFee +
                prioritizationFeeExcludeZero[midIndex].prioritizationFee) /
                2,
            );
          }
        }

        const averageFeeExcludingZeros = Math.floor(
          totalFees / prioritizationFeeExcludeZero.length,
        );

        return {
          maxFee: maxFee,
          minFee: minFee,
          averageFeeExcludingZeros: averageFeeExcludingZeros,
          averageFeeIncludingZeros: averageFeeIncludingZeros,
          medianFee: medianFee,
        };
      } catch (error) {
        console.log(error);
        return null;
      }
    };

  const createTransferSPLTx = async (
    from: string,
    to: string,
    mint: string,
    amount: number | bigint,
    decimals: number,
  ) => {
    const prioritizationFees = await getRecentPrioritizationFees();
    const microLamports = prioritizationFees?.medianFee ?? 1000;

    const setComputeUnitPriceInstruction =
      ComputeBudgetProgram.setComputeUnitPrice({
        microLamports: microLamports,
      });

    const setComputeUnitLimitInstruction =
      ComputeBudgetProgram.setComputeUnitLimit({
        units: 200000,
      });

    const mintPublicKey = new PublicKey(mint);
    const fromPublicKey = new PublicKey(from);
    const toPublicKey = new PublicKey(to);

    const transaction = new Transaction();

    const fromAssociatedTokenAddress = await getAssociatedTokenAddress(
      mintPublicKey,
      fromPublicKey,
    );
    const toAssociatedTokenAddress = await getAssociatedTokenAddress(
      mintPublicKey,
      toPublicKey,
    );
    const toAssociatedTokenAccountBuffer = await solanaClient!.getAccountInfo(
      toAssociatedTokenAddress,
    );

    const transferCheckedInstruction = createTransferCheckedInstruction(
      fromAssociatedTokenAddress,
      mintPublicKey,
      toAssociatedTokenAddress,
      fromPublicKey,
      amount,
      decimals,
    );

    if (toAssociatedTokenAccountBuffer == null) {
      const associatedTokenAccountInstruction =
        createAssociatedTokenAccountInstruction(
          fromPublicKey,
          toAssociatedTokenAddress,
          toPublicKey,
          mintPublicKey,
        );
      transaction.add(
        associatedTokenAccountInstruction,
        transferCheckedInstruction,
        setComputeUnitPriceInstruction,
        setComputeUnitLimitInstruction,
      );
    } else {
      transaction.add(
        transferCheckedInstruction,
        setComputeUnitPriceInstruction,
        setComputeUnitLimitInstruction,
      );
    }

    const recentBlockhash = await solanaClient!.getLatestBlockhash();
    transaction.recentBlockhash = recentBlockhash.blockhash;
    transaction.feePayer = new PublicKey(from);

    return transaction;
  };

  const createTransferSOLTx = async (
    from: string,
    to: string,
    lamports: number,
  ) => {
    const prioritizationFees = await getRecentPrioritizationFees();
    const microLamports = prioritizationFees?.medianFee ?? 1000;

    const setComputeUnitPriceInstruction =
      ComputeBudgetProgram.setComputeUnitPrice({
        microLamports: microLamports,
      });

    const setComputeUnitLimitInstruction =
      ComputeBudgetProgram.setComputeUnitLimit({
        units: 200000,
      });

    const transferInstruction = SystemProgram.transfer({
      fromPubkey: new PublicKey(from),
      toPubkey: new PublicKey(to),
      lamports: lamports * LAMPORTS_PER_SOL,
    });

    const transaction = new Transaction().add(
      transferInstruction,
      setComputeUnitPriceInstruction,
      setComputeUnitLimitInstruction,
    );

    const recentBlockhash = await solanaClient!.getLatestBlockhash();
    transaction.recentBlockhash = recentBlockhash.blockhash;
    transaction.feePayer = new PublicKey(from);

    return transaction;
  };

  const signTx = async (tx: Transaction): Promise<Buffer> => {
    const serializedTransaction = tx
      .compileMessage()
      .serialize()
      .toString('hex');
    const signedTx = await solanaSignTx(serializedTransaction);
    tx.addSignature(tx.feePayer!, Buffer.from(signedTx as string, 'hex'));

    return tx.serialize();
  };

  const sendRawTx = async (serializedTx: Buffer) => {
    return await solanaClient!.sendRawTransaction(serializedTx);
  };

  return {
    getRecentPrioritizationFees,
    createTransferSOLTx,
    createTransferSPLTx,
    signTx,
    sendRawTx,
  };
}
